/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2010  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

/*
 * Support for vpn plugins.  Common code to manage a provider object,
 * tun device and a task associated with an external vpn process.  The
 * vpn plugin is responsible for launching the external process and
 * handling notification callbacks to clock the provider state machine.
 *
 * TODO(sleffler) seems to make more sense in src than plugins
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <linux/if_tun.h>
#include <net/if.h>

#include <dbus/dbus.h>

#include <glib.h>

#include <connman/provider.h>
#include <connman/log.h>
#include <connman/rtnl.h>
#include <connman/task.h>
#include <connman/inet.h>

#include "vpn.h"

#define _DBG_VPN(fmt, arg...) DBG(DBG_VPN, fmt, ## arg)

struct vpn_data {
	struct connman_provider *provider;
	char *if_name;
	unsigned flags;
	struct connman_rtnl rtnl;
	enum vpn_state state;
	struct connman_task *task;
	void *vpn_specific_data;
};

struct vpn_driver_data {
	const char *name;
	const char *program;
	struct vpn_driver *vpn_driver;
	struct connman_provider_driver provider_driver;
};

static GHashTable *driver_hash = NULL;

static int kill_tun(struct connman_provider *provider)
{
	struct vpn_data *data = connman_provider_get_data(provider);
	struct vpn_driver_data *vpn_driver_data;
	const char *name;
	struct ifreq ifr;
	int fd, err;

	if (data == NULL)
		return -1;

	name = connman_provider_get_driver_name(provider);
	vpn_driver_data = g_hash_table_lookup(driver_hash, name);

	if (vpn_driver_data != NULL && vpn_driver_data->vpn_driver !=NULL &&
		vpn_driver_data->vpn_driver->flags == VPN_FLAG_NO_TUN)
		return 0;

	memset(&ifr, 0, sizeof(ifr));
	ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
	strncpy(ifr.ifr_name, data->if_name, sizeof(ifr.ifr_name));

	fd = open("/dev/net/tun", O_RDWR);
	if (fd < 0) {
		err = -errno;
		connman_error("Failed to open /dev/net/tun to device %s: %s",
			      data->if_name, strerror(errno));
		return err;
	}

	if (ioctl(fd, TUNSETIFF, (void *)&ifr)) {
		err = -errno;
		connman_error("Failed to TUNSETIFF for device %s to it: %s",
			      data->if_name, strerror(errno));
		close(fd);
		return err;
	}

	if (ioctl(fd, TUNSETPERSIST, 0)) {
		err = -errno;
		connman_error("Failed to set tun device %s nonpersistent: %s",
			      data->if_name, strerror(errno));
		close(fd);
		return err;
	}
	close(fd);
	_DBG_VPN("Killed tun device %s", data->if_name);
	return 0;
}

void vpn_died(struct connman_task *task, void *user_data,
    enum connman_provider_error error)
{
	struct connman_provider *provider = user_data;
	struct vpn_data *data = connman_provider_get_data(provider);

	_DBG_VPN("provider %p data %p", provider, data);

	if (data != NULL) {
		kill_tun(provider);
		connman_provider_set_data(provider, NULL);
		connman_rtnl_unregister(&data->rtnl);

		_DBG_VPN("provider %p vpn state %d", provider, data->state);
		switch (data->state) {
		case VPN_STATE_CONNECT:
		case VPN_STATE_RECONNECT:
			connman_provider_indicate_error(provider, error);
			break;
		default:
			connman_provider_set_state(provider,
						CONNMAN_PROVIDER_STATE_IDLE);
			break;
		}
	}

	connman_provider_set_index(provider, -1);
	connman_provider_set_interface(provider, NULL);
	if (data != NULL) {
		connman_provider_unref(data->provider);
		g_free(data);
	}

	connman_task_destroy(task);
}

int vpn_set_ifname(struct connman_provider *provider, const char *ifname)
{
	struct vpn_data *data = connman_provider_get_data(provider);
	int index;

	_DBG_VPN("provider %p ifname %s", provider, ifname);

	if (data == NULL) {
		_DBG_VPN("%s: provider data not accessible", __func__);
		return -EIO;
	}

	if (ifname == NULL) {
		_DBG_VPN("%s: ifname not provided", __func__);
		return -EIO;
	}

	index = connman_inet_ifindex(ifname);
	if (index < 0) {
		_DBG_VPN("%s: could not get ifindex from %s", __func__, ifname);
		return -EIO;
	}

	data->if_name = (char *)g_strdup(ifname);
	connman_provider_set_index(provider, index);
	connman_provider_set_interface(provider, data->if_name);

	return 0;
}

void *vpn_get_specific_data(struct connman_provider *vpn)
{
	struct vpn_data *data;

	data = connman_provider_get_data(vpn);
	if (data == NULL)
		return NULL;
	return data->vpn_specific_data;
}

void vpn_set_specific_data(struct connman_provider *vpn,
			   void *vpn_specific_data)
{
	struct vpn_data *data;

	data = connman_provider_get_data(vpn);
	if (data == NULL)
		return;
	data->vpn_specific_data = vpn_specific_data;
}

static void vpn_newlink(void *user_data, int index, unsigned short type,
    const char *ifname, unsigned flags, int change)
{
	struct connman_provider *provider = user_data;
	struct vpn_data *data;

	data = connman_provider_get_data(provider);
	_DBG_VPN("provider %p vpn state %d cur flags 0x%x new flags 0x%x",
	    provider, data->state, data->flags, flags);

	if ((data->flags & IFF_UP) != (flags & IFF_UP)) {
		if (flags & IFF_UP) {
			data->state = VPN_STATE_READY;
			connman_provider_set_state(provider,
					CONNMAN_PROVIDER_STATE_READY);
		}
	}
	data->flags = flags;
}

static DBusMessage *vpn_notify(struct connman_task *task,
			DBusMessage *msg, void *user_data)
{
	struct connman_provider *provider = user_data;
	struct vpn_data *data = connman_provider_get_data(provider);
	struct vpn_driver_data *vpn_driver_data;
	const char *name;
	enum vpn_state state;

	name = connman_provider_get_driver_name(provider);
	vpn_driver_data = g_hash_table_lookup(driver_hash, name);
	if (vpn_driver_data == NULL)
		return NULL;

	/*
	 * NB: Drivers return a state value but it's just used
	 * to determine success/failure (i.e. it does not, for
	 * example, indicate the new state of the vpn).  This is
	 * inherited from upstream and should be re-done...
	 */
	state = vpn_driver_data->vpn_driver->notify(msg, provider);
	if (state != VPN_STATE_CONNECT) {
		connman_provider_set_state(provider,
					CONNMAN_PROVIDER_STATE_DISCONNECT);
		return NULL;
	}

	if (data->state == VPN_STATE_CONNECT) {
		data->rtnl.index = connman_provider_get_index(provider);
		connman_rtnl_register(&data->rtnl);
		connman_inet_ifup(data->rtnl.index);
	} else if (data->state == VPN_STATE_RECONNECT) {
		data->state = VPN_STATE_READY;
		connman_provider_set_state(provider,
					CONNMAN_PROVIDER_STATE_READY);
	}
	return NULL;
}

void vpn_reconnect(struct connman_provider *provider)
{
	struct vpn_data *data = connman_provider_get_data(provider);

	if (data == NULL) {
		connman_error("%s: no vpn data for provider %p",
		    __func__, provider);
		return;
	}
	/* drop default route, et al so vpn can re-resolve remote hostnames */
	connman_provider_ipconfig_clear(provider);

	data->state = VPN_STATE_RECONNECT;
	connman_provider_set_state(provider, CONNMAN_PROVIDER_STATE_CONNECT);
}

static int vpn_create_tun(struct connman_provider *provider)
{
	struct vpn_data *data = connman_provider_get_data(provider);
	struct ifreq ifr;
	int i, fd, index;
	int ret = 0;

	if (data == NULL) {
		connman_error("%s: called out of order", __func__);
		return -EIO;
	}

	fd = open("/dev/net/tun", O_RDWR);
	if (fd < 0) {
		i = -errno;
		connman_error("%s: failed to open /dev/net/tun: %s",
			      __func__, strerror(errno));
		ret = i;
		goto exist_err;
	}

	memset(&ifr, 0, sizeof(ifr));
	ifr.ifr_flags = IFF_TUN | IFF_NO_PI;

	for (i = 0; i < 256; i++) {
		sprintf(ifr.ifr_name, "vpn%d", i);

		if (!ioctl(fd, TUNSETIFF, (void *)&ifr))
			break;
	}

	if (i == 256) {
		connman_error("%s: failed to find available tun device",
			      __func__);
		close(fd);
		ret = -ENODEV;
		goto exist_err;
	}

	data->if_name = (char *)g_strdup(ifr.ifr_name);
	if (!data->if_name) {
		ret = -ENOMEM;
		goto exist_err;
	}

	if (ioctl(fd, TUNSETPERSIST, 1)) {
		i = -errno;
		connman_error("%s: failed to set tun persistent: %s",
			      __func__, strerror(errno));
		close(fd);
		ret = i;
		goto exist_err;
	}

	close(fd);

	index = connman_inet_ifindex(data->if_name);
	if (index < 0) {
		connman_error("%s: failed to get tun ifindex", __func__);
		kill_tun(provider);
		ret = -EIO;
		goto exist_err;
	}
	connman_provider_set_index(provider, index);
	connman_provider_set_interface(provider, data->if_name);

	return 0;

exist_err:
	return ret;
}

static int vpn_connect(struct connman_provider *provider)
{
	struct vpn_data *data = connman_provider_get_data(provider);
	struct vpn_driver_data *vpn_driver_data;
	const char *name;
	int ret = 0;

	if (data != NULL) {
		_DBG_VPN("%s: data != NULL", __func__);
		return -EISCONN;
	}

	data = g_try_new0(struct vpn_data, 1);
	if (data == NULL)
		return -ENOMEM;

	data->provider = connman_provider_ref(provider);
	data->flags = 0;
	data->task = NULL;
	data->state = VPN_STATE_IDLE;

	/* NB: index set when register'ing, may not be available here */
	RTNL_INIT(&data->rtnl,
	    connman_provider_get_ident(provider),	/* name */
	    CONNMAN_RTNL_PRIORITY_DEFAULT,		/* priority */
	    CONNMAN_RTNL_DEVICE_ANY,
	    provider					/* private */
	);
	data->rtnl.newlink = vpn_newlink;

	connman_provider_set_data(provider, data);

	name = connman_provider_get_driver_name(provider);
	vpn_driver_data = g_hash_table_lookup(driver_hash, name);

	if (vpn_driver_data != NULL && vpn_driver_data->vpn_driver != NULL &&
		vpn_driver_data->vpn_driver->flags != VPN_FLAG_NO_TUN) {

		ret = vpn_create_tun(provider);
		if (ret < 0)
			goto exist_err;
	}

	data->task = connman_task_create(vpn_driver_data->program);

	if (data->task == NULL) {
		ret = -ENOMEM;
		kill_tun(provider);
		goto exist_err;
	}

	if (connman_task_set_notify(data->task, "notify",
					vpn_notify, provider)) {
		ret = -ENOMEM;
		kill_tun(provider);
		connman_task_destroy(data->task);
		data->task = NULL;
		goto exist_err;
	}

	ret = vpn_driver_data->vpn_driver->connect(provider, data->task,
							data->if_name);
	if (ret < 0) {
		kill_tun(provider);
		connman_task_destroy(data->task);
		data->task = NULL;
		goto exist_err;
	}

	_DBG_VPN("%s started with dev %s",
		vpn_driver_data->provider_driver.name, data->if_name);

	data->state = VPN_STATE_CONNECT;

	connman_provider_set_state(provider, CONNMAN_PROVIDER_STATE_CONNECT);

	return -EINPROGRESS;

exist_err:
	connman_provider_set_index(provider, -1);
	connman_provider_set_interface(provider, NULL);
	connman_provider_set_data(provider, NULL);
	connman_provider_unref(data->provider);
	g_free(data);

	return ret;
}

static int vpn_probe(struct connman_provider *provider)
{
	return 0;
}

static int vpn_disconnect(struct connman_provider *provider)
{
	struct vpn_data *data = connman_provider_get_data(provider);
	struct vpn_driver_data *vpn_driver_data;
	const char *name;

	_DBG_VPN("disconnect provider %p data %p", provider, data);

	if (data == NULL)
		return 0;

	name = connman_provider_get_driver_name(provider);
	vpn_driver_data = g_hash_table_lookup(driver_hash, name);
	if (vpn_driver_data->vpn_driver->disconnect)
		vpn_driver_data->vpn_driver->disconnect();

	connman_rtnl_unregister(&data->rtnl);

	data->state = VPN_STATE_DISCONNECT;
	connman_task_stop(data->task);

	return 0;
}

static int vpn_remove(struct connman_provider *provider)
{
	struct vpn_data *data;

	data = connman_provider_get_data(provider);
	if (data == NULL)
		return 0;

	connman_rtnl_unregister(&data->rtnl);
	connman_task_stop(data->task);

	g_usleep(G_USEC_PER_SEC);
	kill_tun(provider);
	connman_provider_set_data(provider, NULL);
	return 0;
}

int vpn_register(const char *name, struct vpn_driver *vpn_driver,
			const char *program)
{
	struct vpn_driver_data *data;

	_DBG_VPN("name %s program %s", name, program);

	data = g_try_new0(struct vpn_driver_data, 1);
	if (data == NULL)
		return -ENOMEM;

	data->name = name;
	data->program = program;

	data->vpn_driver = vpn_driver;

	data->provider_driver.name = name;
	data->provider_driver.disconnect = vpn_disconnect;
	data->provider_driver.connect = vpn_connect;
	data->provider_driver.probe = vpn_probe;
	data->provider_driver.remove = vpn_remove;
	data->provider_driver.append_props = vpn_driver->append_props;
	data->provider_driver.save_props = vpn_driver->save_props;
	data->provider_driver.load_props = vpn_driver->load_props;

	if (driver_hash == NULL) {
		driver_hash = g_hash_table_new_full(g_str_hash,
							g_str_equal,
							NULL, g_free);
	}

	g_hash_table_insert(driver_hash, (char *)name, data);

	connman_provider_driver_register(&data->provider_driver);

	return 0;
}

void vpn_unregister(const char *name)
{
	struct vpn_driver_data *data;

	data = g_hash_table_lookup(driver_hash, name);
	if (data == NULL)
		return;

	connman_provider_driver_unregister(&data->provider_driver);

	g_hash_table_remove(driver_hash, name);

	if (g_hash_table_size(driver_hash) == 0)
		g_hash_table_destroy(driver_hash);
}
